Skip to content

FM Synth 0.1.5: scale quantizer on MIDI tracks (the author's 2.13 quantizer) - #555

Closed
timhastie wants to merge 7 commits into
repeat98:mainfrom
timhastie:fm-synth-0.1.5
Closed

timhastie wants to merge 7 commits into
repeat98:mainfrom
timhastie:fm-synth-0.1.5

Conversation

@timhastie

@timhastie timhastie commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

FM Synth 0.1.5: scale quantizer on MIDI tracks (the author's 2.13 quantizer)

This is an author update of FM Synth (synth), which I am registered for. It moves the quantizer bundled inside FM Synth to my octatrick 2.13 line, timhastie/octatrick-modules at 48b89e7e5315971bf5c29a3fd86183abe8df48f3 (tag v2.13). Version 0.1.4-experimental -> 0.1.5-experimental; the catalog pin, release notes and changelog are updated. The FM engine, the FM SYNTH chooser and the page are unchanged from 0.1.4: at 48b89e7 my synth/ files are the same bytes as at v2.12.

0.1.4 was not submitted on its own, so this PR carries both: 0.1.4's two commits, then 0.1.5's five (source and documents, packages, evidence revision, README wording, and my hardware report). The branch is rebased onto main c181b76b0155c68db4f0efc558f34cfa3a5c8b58 (fetched 13 Oct 2026, 08:50 PDT; the rebase applied without conflicts). At 08:58 PDT main was at 23556edd; the commits since c181b76 change only documentation (AGENTS.md, CHANGELOG.md, docs/ADD_A_MODULE.md, docs/APP_DEVELOPMENT.md, docs/module-guides/, the elekloader README), none of them a file this branch touches. Since the commit the packages and evidence were made on (263aa1b), main has changed nothing in this module's folder, in sdk/octabam/, in sdk/catalog.json or in the generated assets this branch rebuilds, and a rebuild from the rebased head gives the same image (below).

What changes from 0.1.4

SCALE and ROOT act on MIDI tracks too. With a scale on (PROJECT > CONTROL > SEQUENCER, SCALE and ROOT):

  • The NOTE knob of a MIDI track steps to the next note of the scale in the turn direction, from ROOT (MINOR, ROOT A, from C3 up: D3, E3, F3 ...). A note set while the scale was off goes to the nearest scale note in the turn direction. Hook qz_mknob (jsr, 8 B) at 0x40055382, the knob handler's MIDI clamp just before its store 0x4005538a; the store stays stock.
  • NOTE locks do the same, with a TRIG key held and during LIVE RECORDING. Hook qz_mplock (jsr, 10 B) at 0x40050b98.
  • The MIDI CHROMATIC keys play the key's note plus ROOT, snapped to the nearest scale note (ties down), so key 1 sounds the root; the number beside the keyboard reads the root and the octave ("A 3"). The note-on, the note-off, the arpeggiator fed by the keys and LIVE RECORDING carry the snapped note. Hook qz_mkey (jmp, 10 B) at 0x400436a8.

What is not snapped in this version. NOT2–NOT4 stay semitone offsets, TRAN transposes in semitones, the arpeggiator's own notes, MIDI IN, VEL / LEN, CCs, LFOs on NOTE and scene locks are stock. An option for the chords, TRAN and the arpeggiator to follow the scale is planned for a later version, together with a per-track switch that exempts a MIDI track from the scale (a drum machine's note numbers are sounds, not pitches). Until then, with a scale on, every MIDI track's NOTE knob, locks and chromatic keys snap: a drum track is programmed with SCALE OFF, and stored notes are never changed. Stock's own ARP SETUP > KEY still forces everything a track sends into a key, per track. SCALE OFF is stock; nothing new is stored.

The #264 fix is now in my own line. Modwerk's 0.1.2 moved the bundled quantizer's CHROMATIC drawer hook to the common argument push 0x400449be (#264: CHROMATIC on a MIDI track froze the unit). My public line carried the old site from 2.9 to 2.12; 2.13 takes Modwerk's form at the same site, credited to the Modwerk contributors in my quantizer README and LICENSE. So the bundled quantizer.s is my file byte for byte again. FM Synth images were never affected.

Files. upstream/quantizer/quantizer.s, core.s, keys.s, scale.s, README.md and upstream/LICENSE are my 48b89e7 files byte for byte. upstream/quantizer/manifest.py is my 2.13 manifest in Modwerk's stock-guard form: its 23 stock expectations became stock_guard(address, length, SHA-256), each checked against an original MAIN OS 1.40C (164f3122…) first; nothing else in it changed. stock-guards.json: 76 guards (the quantizer's 23, three of them new). The module LICENSE carries my 2.13 LICENSE in full: one added sentence crediting Modwerk's #264 form. The wrapper manifest.py changes its docstring and proof note only. New inventory sdk/imports/synth-48b89e7.json (following 0.1.4's synth-ca13ed8.json). No stock bytes are committed: my tools/stock_scan.py finds no stock run of 16 bytes or more in any data file of the module folder, and the assembled quantizer's hits are the six displaced-instruction replays and assembler idioms of my reviewed 2.13 scan.

Compatibility. The public Scale Quantizer and Analog BD stay declared conflicts. Modwerk's MIDI Scenes builds only on its own, so it never shares an image with FM Synth; its writes cover the knob store 0x4005538a, and the new hook ends just before it. No conflict entry is added. The POLY8 item of 0.1.4 is unchanged (below).

Evidence (emulator, exact image)

The image is MAIN OS 37661aee03a49f310d47e1447f043489c1bce9430c13824dd49ab93ad835e236, built by scripts/export-composition-proofs.py --vendored-sdk --static-stock --modules synth --select synth:true from this branch (rebuilt from the final source commit, and again from the rebased head on main c181b76: the same bytes both times). The update file is b42048d5… (462,628 B, OCTAMOD79); Modwerk's TS packer decodes it to that MAIN OS and re-encodes the same bytes. Pinned ot_emu 9dc31d12…. Details in TESTING.md.

  • Same FM code as 0.1.4. The objects 00_poly.o and 01_fmmachine.o are byte-identical to 0.1.4's, and the platform runtime's first 23,072 bytes (both units) are identical and at the same addresses. Every other image difference is the quantizer's (its DRAM unit, its hook sites and table entries, the appended loader).
  • The quantizer on MIDI track 1 (my 2.13 gate scripts, MIDI OUT captured on the unit's port, a model of the rules): NOTE knob and NOTE locks 409 of 409 over eight SCALE / ROOT settings; CHROMATIC keys 245 of 245 (note-offs balanced, held keys across SCALE / ROOT changes released, the arpeggiator fed snapped notes); CHROMATIC on a MIDI track draws and plays; the LIVE RECORDING lock as on my 2.13 build. SCALE OFF: the same knob script on 0.1.4's image matches the stock clamp 409 of 409. The sequencer (trigs with NOT2–NOT4, NOTE locks at 0 and 127, TRAN, ARP SETUP KEY, the arpeggiator), MIDI IN thru and the auto channel send the same MIDI events as on 0.1.4's image under SCALE OFF, MAJOR C, MINOR A and PENT.MN F#.
  • Cost (emulator instruction counts per call, UI task, not a hardware share): a MIDI CHROMATIC press 6 with SCALE OFF, 49–77 with a scale on; a NOTE knob turn of 1–3 clicks 10 / 104–278; a NOTE lock turn 8 / 117–302. No frame-interrupt code is added.
  • Cycles. 0.1.4's frame-interrupt table carries over (same objects, same addresses); its four bounding cards ran twice more on this image, inside the rig's run-to-run spread. Over all seven runs one VOIC 4 track peaks at 32,314 instr: worst case 24,167 cycles/block, one track with stock reserve and wrapper 81,459 of 95,779 (0.1.4: 24,125 / 81,422). Eight VOIC 4 tracks still do not fit (97,745 instr, 2.54x). Instruction counts converted at the CF METER calibration (9.44 ns, 264 MHz); not chip cycles.
  • Memory. Exact from the link map: 44,404 module-owned bytes (0.1.4: 43,812; the quantizer unit 3,532 -> 3,892 B, three hook sites, the loader), plus the shared 10,460,844 B sample-RAM platform reserve; 10,505,248 B in total. Stack: 352 B, unchanged; the new hooks reach 72 B in the UI task.
  • Part smoke, persistence, machine change. The module's verify.py Part smoke passes on this image; two FM tracks pass Part save/reload, project save/reload and a fresh project load; the 2.12 machine-change init gives FM SYNTH's init both times with FX1 kept. Not testable on the emulator: the power cycle with only battery RAM and the card (on my hardware sheet).
  • Odd addresses. My tools/align_check.py (2.13): 5,843 instruction rows in five linked objects, all even; 78 detour / symbol-ref / poke targets even.
  • Captures. Seven black-and-white LCD captures from this image (media/capture.json): five byte-identical to 0.1.4's, the SEQUENCER window now MINOR / A, and a MIDI track's CHROMATIC page with the "A 3" readout (new). 0.1.4's SELECT MACHINE TYPE and SCALE captures are removed: main's audio preview makes eight media with seven screenshots.

Open: FM Synth with POLY8 (unchanged from 0.1.4)

POLY8's chooser bridge (sdk/octabam/modules/poly8/composition.json, unchanged in main's POLY8 0.2.7) is bound to FM Synth 0.1.2's chooser hooks; my chooser (0.1.4 and this version) makes different claims, so a build with POLY8 and FM Synth stops at POLY8's seam check, npm run module:verify -- synth stops at the first such selection, and sdk/native-comparisons/synth.json stays main's 0.1.2 record. Without POLY8 in the pool (a local run on main 263aa1b, not committed): 122 selections, 35 builds match native outside the platform writes, 87 refusals match, 0 mismatches; the native side ran the same exporter on the host toolchain because Docker is not available here. The same tests fail as for 0.1.4 (TESTING.md, "Composition parity"). I have not changed POLY8's files; I can prepare the bridge update if you want it.

Load, plainly

FM SYNTH and SY DRUM tracks are heavy on processor load, and track freezes with many instances of either are expected. FM Synth runs on the ColdFire inside the audio interrupt. One FM SYNTH track at VOIC 4 takes about 85 % of an audio block on the emulator (81,459 of 95,779 cycles, this record's maxConfiguration), and the record qualifies one instance. More FM SYNTH tracks, or SY DRUM tracks beside them (SY DRUM is submitted separately and shares this engine), overrun the block, and then tracks can stall or the unit can freeze. That is expected with many instances of FM SYNTH or SY DRUM; it is not a defect of a particular project. The figures are emulator cycle counts at the record's calibration, not hardware times; the Processor Load module (submitted separately) shows the headroom on the unit.

Hardware

I tested the exact image above (MAIN OS 37661aee…, FM Synth 0.1.5-experimental, the update file b42048d5…) on my Octatrack. The same facts are in tests.qualification.hardware (hardwareStatus reported, my own report) and in TESTING.md:

  • tested on: 2026-10-13
  • tester: timhastie
  • model: MKI
  • summary: every item of the image's test sheet worked. On a MIDI track the NOTE knob and NOTE locks stepped through the scale, the CHROMATIC keys followed ROOT, chords and the arpeggiator played as before, CHROMATIC trig mode no longer froze the unit, and SCALE OFF behaved as before; FM SYNTH played as in 0.1.4; after SAVE and a power-cycle with the card in, SCALE, ROOT and the FM SYNTH track were kept and the NOTE knob still followed the scale.
  • limitations: play time, the MIDI gear used and the number of tracks were not recorded. It is a functional check of the sheet's items, not a load or stress qualification; the cycle and memory figures above are emulator measurements, not hardware times.

release-notes.json still says the report is pending: that file is part of the native source fingerprint the measurements are bound to, so I left it as measured; the changelog entry, README.md, TESTING.md, docs/VERIFICATION.md and the record carry the report. The record's evidenceRevision / hardware.sourceRevision point to bcf3b7f, the rebased commit of the measured source (the module folder is byte-identical to the measured 7a5988a).

My Octatrick 2.13 build carries the same quantizer source; a report of that build is of a different image and does not qualify this one.

Tape Echo preservation gate

Main c181b76 carries the Tape Echo browser-infrastructure preservation gate (scripts/browser-infrastructure-preservation.mjs, reached from scripts/retained-evidence.mjs). This branch changes generated packages under src/engine/assets/ (the rebuilt FM Synth objects and the re-bound Analog BD record), so npm run modules:check -- --base origin/main and npm run modules:generate both stop at "Invalid or stale Tape Echo browser preservation binding; full qualification is required". I cannot clear that from my side: it needs your binding (or a Tape Echo re-qualification). Because modules:generate cannot run, src/catalog/module-documents.json is not regenerated in this PR: it still lists FM Synth 0.1.2, while sdk/catalog.json, the packages and the changelog are at 0.1.5. A modules:generate after the binding produces it; I am glad to push that commit once the gate passes.

Checks

All on this branch as pushed, rebased on main c181b76, Node 24.21.0, 13 Oct 2026, without switching the gate off:

  • Ran and passed: licenses:check, machines:check, elekloader:check, sdk:imports:check, typecheck, lint (0 errors; 7 warnings, all in files this branch does not touch).
  • Stopped by the gate: modules:check -- --base origin/main and modules:generate (above), and therefore npm run check -- --base origin/main, which stops at modules:check after the licence and machine checks. build:bundle was not run to a result: it needs public/module-media/, which only modules:generate writes.
  • module:doctor -- synth: catalog entry, library, category guide, packages (compiled at 0.1.5), thumbnail and declaration checks pass; "module rules" fails on the gate and "native comparison" on the POLY8 item.
  • SDK tests (python3 -m unittest discover -s sdk/tests, as in sdk:check): 227 of 228 pass (7 skipped); the failure is test_measured_release_keeps_hardware_attribution_separate, the native-comparison record (POLY8 item).
  • npm test (vitest): 2,253 of 2,282 pass. Of the 29 failures, 5 also fail on an untouched checkout of main c181b76 in the same environment: the 4 scripts/module-pages.test.mjs tests (missing public/module-media/, written by modules:generate) and src/tooling/elekloader-update.test.ts, which exceeds its 5 s timeout on this machine; 4 are the POLY8 item (src/engine/requested-modules.test.ts x2, src/catalog/native-comparisons.test.ts, src/catalog/synth-release.test.ts "publishes through measured cycles …"); 2 are the gate itself (src/catalog/module-qualification.test.ts "preserves tapeecho qualification …", and scripts/module-doctor.test.mjs, whose doctor run reports the failing module rules); and 18 come from the catalog that modules:generate could not regenerate: Compiled module versions differ from the catalog (src/engine/module-build.test.ts, compose-os.test.ts, core-logger.test.ts, session.test.ts x4, src/catalog/build-support.test.ts x2), Changelog version is newer than the catalog for synth (src/community/module-changelogs.test.ts, and src/tooling/check.test.ts x7, whose check.mjs runs reach the same changelog check), and the 0.1.5 quick test against a 0.1.2 catalog entry (src/community/downloaded-module-guide.test.ts). On main c181b76 in the same environment: 2,277 of 2,282 (those 5).
  • Image: the native exporter rebuilt FM Synth alone from the rebased head: MAIN OS 37661aee…, update file b42048d5…, the bytes of the image I tested.
  • Stock bytes: my tools/stock_scan.py (2.13) over the module folder finds no run of 16 or more stock bytes in any file; over upstream/ (assembled) it finds the same 49 runs as my reviewed 2.13 scan (displaced-instruction replays and assembler idioms, at most 22 bytes); over every other file this branch changes, the hits differ from main's copies only where FM's package changed, and the runs added there are among those 49.
  • Earlier, on main 263aa1b, where modules:generate was not stopped (a scratch copy with test values in the hardware fields): modules:generate and modules:check passed, a fresh import of the packages reproduced the committed assets, and the only test failures were the POLY8 item and a check.test.ts timeout on this machine.

Review scope

The scope is wider than this module's folder: the branch also carries 0.1.4, and 0.1.5 changes sdk/catalog.json, sdk/imports/ (the 0.1.5 inventory), docs/VERIFICATION.md, the FM Synth release tests (TS and SDK), one module-source test version, the FM quick-test version in src/community/downloaded-module-guide.ts, the changelog and the rebuilt packages and records; the module LICENSE gains the sentence from my LICENSE. So this needs owner review; I have left the automatic-publication boxes unticked.

Checklist

Ticked where the facts above show it for this branch as it stands. The open boxes are the gate (modules:check, module:doctor), the POLY8 item, and the two that are your decisions.

  • Automatically merge and publish my module update after checks pass.

  • I verified the source, licences, UI and qualification reports, including instance isolation and Part/project/reboot persistence where applicable; no required acceptance check is failed or untested.

  • New or changed module: I read the guide for its category in docs/module-guides/ (and sequencing.md if it acts in time), walked its checklists, and TESTING.md lists what I did not test. npm run module:doctor -- <id> is green.

  • Every code change to a module has a greater semantic version and exact catalog pin (documentation and media edits need none).

  • Source and media are original or properly licensed; authors and full licence texts are preserved.

  • No Elektron firmware, extracted routines/tables, upgrade files or other infringing material is included.

  • Every new/changed module with OT UI has real screenshots of its location and relevant control pages, manifest access instructions, and media[].otUi version/build/setup provenance.

  • I verified the captions, button/menu steps, screenshot authenticity and page coverage against the current module UI.

  • TESTING.md records commands, results, exact source, conditions and limitations; emulator and hardware evidence are separate.

  • CPU, DSP core and memory gauges are populated in resources.impact with rough load tiers, workload, rationale and source records.

  • Runtime/resource changes and new modules: tests.qualification contains worst-case cycle counts for every processor used, under parameter extremes, simultaneous modulation, mode changes and maximum load, within the declared real-time budget.

  • Runtime/resource changes and new modules: exact memory regions include code, state, tables, buffers, stack/heap, padding and shared allocations; words, word widths, bytes and maximum-instance totals agree with the native allocation/build report.

  • Runtime/resource changes and new modules: this version/source/build has attributed real-hardware test evidence verified by the reviewer (the owner for first releases; the registered author for scoped updates).

  • Complete README/TESTING/licence/control/compatibility documentation, at least three practical tutorial steps and real black-and-white PNG screenshots document the current behavior with honest version/build provenance.

  • I rebased onto current main and ran module/version checks on this revision.

Credits

  • Tim Hastie: FM engine, voices and chords, MIDI and legato, the chooser (adapted from Modwerk's), the machine-list table and machine-change init, the quantizer and its 2.13 MIDI-track mode.
  • Sam Banks: octabam wrapper and platform, pinned 949f3be.
  • Modwerk contributors: the original dedicated chooser and Part validation (0.1.1), the quantizer drawer fix for [synth] error #264 (0.1.2), whose form my 2.13 quantizer takes, verification, docs and the original SVG.

Full MIT terms are in the module's LICENSE.

How to test

  1. npm ci, then npm run check -- --base origin/main and npm run module:doctor -- synth (both stop at the Tape Echo gate until it is bound, see above).
  2. In the configurator, select FM Synth alone and download. Install it with PROJECT > OS UPGRADE.
  3. PROJECT > CONTROL > SEQUENCER: SCALE MINOR, ROOT A. On a MIDI track's NOTE page turn NOTE from C3: D3, E3, F3 ... Hold a TRIG key and turn NOTE: the lock steps the same way.
  4. Trig mode CHROMATIC on the MIDI track: the readout reads "A 3", key 1 plays A3, the keys play A minor. A chord (NOT2–NOT4) and the arpeggiator play as before.
  5. SCALE OFF: the MIDI track behaves as stock.
  6. FM SYNTH (FUNC + SRC, the sixth row) plays as in 0.1.4.

…13ed8)

The engine, page and the author's FM SYNTH chooser are vendored byte for byte from
timhastie/octatrick-modules v2.12 (ca13ed8): the 2.10 engine with 6 dB of headroom, the 2.11
machine list (a row table, six row-count detours in place of the 2.10 pokes, five row-clamp
detours, the SELECT MACHINE TYPE scroll) and the two 2.12 changes: the metronome click no
longer cuts a held note (the engine's hook on the click word is removed), and a machine change
to or from FM SYNTH resets the track's machine parameters to init, step and scene locks kept as
in stock (one new detour, po_unload). With FM Synth alone the list keeps its six rows.
Modwerk's machine.s / registration.s are dropped; the bundled quantizer keeps 0.1.2's drawer
fix (repeat98#264). Every stock expectation is a stock_guard (73). The author's native Analog BD
conflict is cleared in the wrapper (Modwerk declares it in octamod.module.json).

Emulator evidence on the image built from this source (MAIN OS 00fa6caa): cycles (one VOIC 4
track 81,422 of 95,779 cycles/block, the largest of five runs), exact memory (43,812
module-owned bytes), stack bound 352 B, two-instance Part/project save and reload, the
metronome case and the machine-change init, captures, Part smoke, odd-address check. Hardware
fields are placeholders for the author's report. The native/browser record is not refreshed:
POLY8's bridge is bound to 0.1.2's chooser hooks (TESTING.md).
…racks)

The bundled quantizer moves to timhastie/octatrick-modules 48b89e7 (tag v2.13; its synth/
files are the same bytes as v2.12 ca13ed8, so the engine, chooser and page are 0.1.4's). With a
scale on, a MIDI track's NOTE knob and NOTE locks step through the scale from ROOT and its
CHROMATIC keys play the scale from ROOT (three new detours: 0x40055382 qz_mknob, 0x40050b98
qz_mplock, 0x400436a8 qz_mkey); NOT2..4, TRAN, the arpeggiator, MIDI IN and SCALE OFF are
unchanged. 2.13 takes Modwerk's repeat98#264 form of the CHROMATIC drawer hook (0x400449be), so
quantizer.s, core.s, keys.s, scale.s and README.md are the author's files byte for byte; the
quantizer manifest is his 2.13 manifest in stock_guard form (23 guards). The module LICENSE
carries his 2.13 LICENSE (one sentence crediting Modwerk's form). Inventory
sdk/imports/synth-48b89e7.json, following 0.1.4's synth-ca13ed8.json.

Emulator evidence on the image built from this source (MAIN OS 37661aee): the FM objects are
byte-identical to 0.1.4's at the same addresses; NOTE knob / locks 409 of 409 and CHROMATIC keys
245 of 245 against a model of the rules, SCALE OFF as stock, the sequencer unchanged, the
quantizer's added cost per call; Part smoke, two-instance persistence, machine-change init;
exact memory (44,404 module-owned bytes), stack bound 352 B; 0.1.4's cycle table with its
bounding cards rerun here; seven captures (a MIDI track's CHROMATIC page new; two 0.1.4
captures dropped for the eight-media limit). Hardware fields are placeholders for the author's
report.
…nly); Analog BD record re-verified and rebound

build-module-packages.py --include-requested on this source, imported with --development: only
the Synth objects and the Synth version change against main's 0.1.2 packages (Modwerk's
fmregistration object is gone, the author's poly and fmmachine objects come in, the bundled
quantizer objects change; 0.1.4 has no package commit of its own on this branch). The Analog BD
record's moduleSourceTreeSha256 is rebound after verify-analog-bd-native.mjs passed against the
new packages (162 native OS and GNU bootloader comparisons, 50 matching refusals, 6 firmware
round trips).
… scale, and a per-track exemption, are planned; a drum track is programmed with SCALE OFF until then
…evidence revision bcf3b7f (the measured source on main c181b76); import inventory refreshed
@repeat98

Copy link
Copy Markdown
Owner

Is this ready to merge?

@timhastie

timhastie commented Oct 13, 2026 via email

Copy link
Copy Markdown
Contributor Author

@repeat98

Copy link
Copy Markdown
Owner

Thanks for 0.1.5. I tried to bring it up to date with main today, and it can't be merged yet. It needs more than a refresh. Main has moved a lot since 13 October: the Octatrack base now owns the machine table and several MIDI, chromatic and project events, and POLY8 compositions are checked against FM Synth. 0.1.5 hasn't been checked against that yet.

I merged main into your branch locally, moved your notes to src/community/module-changelogs/synth.json, rebuilt the packages in the pinned image and regenerated the catalogue. Your Synth objects come out byte for byte as in your own build. I did not push that to your branch.

What fails on that merge, all without firmware:

  • npm run modules:compat: 95 problems. 94 are FM Synth patching stock sites the base now owns (the machine table ot_machines, sequencer rows, MIDI note on/off, chromatic, project load/write), and 1 is a POLY8 composition seam drift at 0x400786c8.
  • npm run test: 4 files fail. They are the POLY8 companion tests ("Machine composition seam drift: synth"), module-doctor, synth-release.test.ts, and native-comparisons.test.ts. The last two read the native comparison record, which is still the one for 0.1.2. Re-running it needs npm run module:verify -- synth --os <OS 1.40C file>.
  • npm run sdk:check: 20 failures, mostly the module combination tests ("install in every combination through events and tables").

Lint, typecheck, the bundle, the catalogue, licences and the package rebuild pass.

How FM Synth 0.1.5 registers on the base has to be settled on our side before there is anything useful for you to change, so please don't spend time on rebasing it for now. I'll write here again when I know what is needed.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks for the base update! Is the FM Synth 0.1.5 registration blocker now resolved, or should I still wait before rebasing? If it’s ready, what changes are needed before I rebuild and rerun verification—and does the same guidance apply to SY DRUM?

@timhastie

Copy link
Copy Markdown
Contributor Author

Holding as you asked. When you confirm the new Octatrack base and how a module registers with it, I will re-cut the module from my current 2.18 source instead of 0.1.5, so one update carries the fixes reported in #751, #589 and #557. Do you have an ETA?

@repeat98

Copy link
Copy Markdown
Owner

Hi Tim, sorry for the wait, and thanks for holding. You can start now. FM Synth 0.1.2 on main shows how a machine registers on the base. Your files stay byte for byte under upstream/, and sdk/machines/octatrack/module-adapters/machine-ports.json maps each of your hooks on a base-owned site to a base event (po_mon to ev_midi_note_on, qz_octkey to ev_octave_key and so on). The rest stay guarded patches. For 2.18: a fresh branch from current main, your files in upstream/, then npm run modules:compat lists every hook that needs a line there. If that part gets fiddly I can do it. The USB package comes from sdk/runtime/loader/build.py --octabam-machine synth. SY DRUM follows the same rule as before: it has to build and work without FM Synth. Two FM Synth PRs are open on the same files: my #895 (0.1.3, card builds only) and Jan's #884 (the #883 fixes). Could you look at #884 and say if you're OK with it or want it in your line? I'll close this one when your 2.18 PR is up.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks, Jannik! I'm starting now: a fresh branch from main with my current line under upstream/, the machine-ports.json lines that modules:compat asks for, and the USB package from build.py. Understood on SY DRUM: I'll make it build and work without FM Synth. I'll answer on #884 separately. Please close this PR when the new one is up.

@timhastie

Copy link
Copy Markdown
Contributor Author

Hi Jannik, thanks again for unblocking the re-cuts. Before I build SY DRUM, a quick question about which way you'd like it. I'd like people to be able to use SY DRUM and FM Synth in the same project, and I see two ways:

  1. SY DRUM inside FM Synth as a second machine, following "ship both features as one module". It would come as an FM Synth update with card builds, like your 0.1.3.
  2. SY DRUM as its own module that works without FM Synth and installs beside it. That needs base events for the sites the two share (voice path, LFOs, step locks, scenes and a few keys), and I'd write those PRs for you to review. Its step locks need about 2 MiB, more than a USB package can hold today, so its USB package would also need larger memory regions in the loader.

Both need a fifth slot in ot_machines. Which would you prefer?

@timhastie

Copy link
Copy Markdown
Contributor Author

A few more things I'd like to check with you before I prepare the PRs:

  1. Tuner: its window leaves a close callback and key layer in the host UI, and the base has no removal check for ordinary modules, so a USB removal while it's open could crash. I'd write a base PR that refuses removal while a module's UI is open. Is that the way you'd like it, or do you have another route in mind?
  2. FM Synth 0.1.4's step-lock card files hook the engine's job dispatcher at 0x4008485e, where the base has its logger hook. I'd propose an ev_engine_job take event there; the base would then run its logger through that event. OK with you?
  3. Two FM Synth 0.1.4 hooks start just before sites ot_machines owns: the LFO destination names at 0x4003bff2 and the chooser commit at 0x40079816. If the base's machine table already does both jobs, I'd let ot_machines' replaces cover them. Does it?
  4. Euclid copies 5 bytes from inside FM Synth's step-lock clip hook at 0x4002b654. The same bytes sit unpatched at 0x4002c16b. Is it OK if Euclid copies them from there instead? Its package would then need a rebuild and a version step.

Thanks!

@repeat98

Copy link
Copy Markdown
Owner

Hi Tim, thanks for asking first.

  1. SY DRUM: option 1, inside FM Synth as a second machine. Option 2 means a set of shared base events whose only users would be FM Synth and SY DRUM, plus bigger USB regions, and I'd rather not grow the base for that now. Please keep FM Synth's USB package working; if SY DRUM's step locks can't fit a package yet, tell me what it needs. Both need a fifth row in ot_machines; FRACT needs one too, so I'll raise it in its own base PR and tag you.
  2. Tuner: the base already refuses to retire a machine while a reviewed stock UI root still names its image (machine_contributors.c, machine_lifetime.h). I'd extend that check to every module rather than add a second mechanism, and a PR for it is welcome.
  3. ev_engine_job: OK. The logger records engine job begin and end at that dispatcher, so a take event it subscribes to fits; please send it as its own base PR.
  4. The base owns 0x4003bff4 (the PB table by machine row) and 0x4007981c (the chooser commit, which calls a machine's assign/admit_track). Your hooks start 2 and 6 bytes before those, so they'd overlap. For the commit, use assign/admit_track in your ot_machines row. For the LFO destination names, tell me what that hook does: the base only swaps the PB table pointer.
  5. Euclid is mine, so yes, copy the bytes from 0x4002c16b as long as nothing in them is PC-relative; Euclid then needs a rebuild and a version step.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks Jannik, that settles it.

  1. SY DRUM goes into FM Synth as a second machine. I'll wait for your ot_machines PR for the fifth row. On USB: FM Synth 0.1.4 is already held at the 0.1.2 package with a declared lag, because the loader refuses the DRAM regions its step-lock and scene tables need (about 2 MiB, 32 KiB, 2.5 KiB and 256 KiB) and its generated includes. SY DRUM adds a 2 MiB lock table and a 64 KiB scene table. I'll write up exactly what the loader would need in the 0.1.5 PR.
  2. Tuner: I'll extend the existing check to every module and send it as a PR.
  3. ev_engine_job as its own PR, with the base logger subscribing to it.
  4. The chooser commit moves to assign/admit_track in my row. The LFO-names hook: on an FM track, LFO SETUP's PMTR formatter picks the PLAYBACK descriptor by the track's machine byte, which is FLEX's in my line, so the hook swaps in FM's own page descriptor and the list reads PTCH RATO INDX FINE FDBK DEC instead of FLEX's names. I'm checking whether your PB-table swap already does that for a contributed row; if it does, the hook goes.
  5. Euclid: I'll check the five bytes for anything PC-relative and send the copy change with a rebuild and version step.

One more: my 2.19 quantizer adds seven MIDI-track sites (the MIDI knob, p-lock, key, note, arp trig, preview and held-trig mark) that FM Synth's bundled copy and the standalone quantizer both patch, so the two would overlap. I'm preparing take events for them like the existing ev_knob_store / ev_plock_store. OK with you, or would you rather I handle that another way?

@timhastie

Copy link
Copy Markdown
Contributor Author

Hi Jannik, three things from building against your answers:

  1. The LFO-names hook, your question: on an FM track, LFO SETUP's PMTR formatter picks the PLAYBACK page from the machine table by the track's raw machine byte. A contributed machine's track keeps byte 0 or 1, so your pointer swap at 0x4003bff4 still gives the FLEX page. I tested with and without the hook: PTCH RATO INDX FINE FDBK DEC against PTCH STRT LEN RATE RTRG RTIM. For the base to cover it, the table would have to own the 8 bytes at 0x4003bff2 and resolve the page through mw_page first, as mw_resolve_pb does. Until then the hook stays a declared patch. The chooser commit now goes through assign/admit_track in my row. To keep that card-build hook out of the USB package I added a small dropDetours key to machine_ports.py, which is base-side adapter code in my PR; happy to change the form if you prefer another.

  2. For your fifth-row PR: with SY DRUM as a second machine, my module contributes two ot_machines records. machine_registry.c has ROWS 9 and the loader and build.py take one record per module per table, so raising the room alone isn't enough; the base would need to accept two records from one module. Also SY DRUM's SETUP page editor hook at 0x4003a524 (10 bytes) overlaps the table's pointer at 0x4003a526; a setup_edit field or event would cover it. The USB needs you asked for: FM Synth 0.1.4 alone needs four DRAM regions (2 MiB, 32 KiB, 2.5 KiB and 256 KiB) plus generated includes. With SY DRUM it's six regions, about 4.5 MB, and the module image is about 234 KiB against the 128 KiB cap.

  3. The removal check extended to every module, as you suggested, works with one catch. After the Tuner closes its window, stock's window manager keeps the close callback in its slot and calls it when the next window takes that slot, so the fail-closed check still refuses removal until the TEMPO window has opened once. Two ways out: (a) at the switch, the base zeroes a closed slot's stale callback that names the leaving module (WCREATE skips a zero callback); (b) the Tuner clears its own slot when it closes. I'd do (b), in the module. OK?

One more: my ev_engine_job draft sits at index 35, which #912 now holds; I'll renumber at merge.

@repeat98

Copy link
Copy Markdown
Owner

Hi Tim, thanks, that's a lot of useful detail. In the order you asked:

Quantizer, the seven MIDI-track sites. Some may already be covered. #857 added ev_midi_note_key (0x4009fb58, every note from a trig, chord or arp step, before KEY), ev_arp_setup_knob (0x4007a2ec) and ev_arp_setup_text (0x4007a1ec). The MIDI p-lock may also already be ev_plock_store's site (0x40050e60, in the held-trig lock editor). Could you list the seven addresses so we can see which are left?

For the rest, I'd rather not add events whose only users are two copies of your own quantizer. FM Synth doesn't need MIDI-track quantizing for its own tracks. So please put that part in Scale Quantizer only, and keep FM's bundled copy to what FM tracks need (the events it already uses). Sites that only Scale Quantizer patches then stay ordinary guarded patches. We add an event when a second module needs one. One thing to watch: #954 (ARP KEY LOCKS) also takes ev_midi_note_key, so only claim the tracks you actually quantize.

1. LFO SETUP's PMTR names. You're right. That instruction indexes the table by the track's raw byte, and a machine's track keeps 0 or 1, so the pointer swap gives FLEX's page. Analog BD has the same problem, so it's the base's to fix: ot_machines takes the whole instruction at 0x4003bff2 and resolves the page through mw_page, as mw_resolve_pb does. A small base PR from you is welcome, or I'll do it. Until then please don't keep the hook as a patch. It overlaps a site the table owns, so modules:compat refuses it. The package just shows FLEX's names there until the fix lands.

On dropDetours: I'd rather not add a new key. replaced() in build.py only accepts a table detour that starts inside an owned site. Widen it to a detour that overlaps one, and list the hook under the ot_machines contribution's replaces. Then the drop is checked against the table's sites, which a free list isn't.

2. SY DRUM. Thanks for spelling out the one-record limit. I don't want to change the table contract for this. Could SY DRUM be a second engine on FM Synth's own row instead, the way Analog BD opens its engine list (POOL, pool_open, list_draw), with track_page and render picking per track? FM Synth then keeps one record, and the extra rows go to the new machines waiting for one (FRACT, and WAVE in #1028). If there's a reason it has to be its own chooser row, tell me.

For the SETUP editor at 0x4003a524: please check first whether track_page plus the SETUP_ALL flag (on main since #1017) covers it. That's how Analog BD edits its SETUP page over USB. If it still doesn't, I'd take a field in the machine record rather than an event.

USB: thanks for the numbers. The loader won't get multi-MB DRAM regions or a bigger image for now. FM Synth's USB package stays at 0.1.2 with the declared lag, and SY DRUM ships in card builds. One question: a track is one machine at a time, so could FM and SY DRUM share one lock table instead of two 2 MiB ones?

3. Tuner. (b) is good: the Tuner clears its own slot when it closes, and the base's check stays read-only. Please add a line about it to docs/module-guides/standalone.md so the next module with a window knows.

ev_engine_job: yes, take the next free index when it's ready to merge. That's 37 today, but #1023 and #1029 both claim 37 already, so check main at merge time; whoever merges later renumbers and rebuilds the base.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks Jannik, that's all clear, and I'll send the quantizer addresses separately.

On SY DRUM, I'd like to push back a little before changing course. On 26 Oct you wrote:

SY DRUM: option 1, inside FM Synth as a second machine. [...] Both need a fifth row in ot_machines; FRACT needs one too, so I'll raise it in its own base PR and tag you.

I built FM Synth 0.1.5 to exactly that: SY DRUM inside FM Synth, registered as a second machine with its own ot_machines record, waiting only on the extra row. It's built, and the card test image passed every item on my test sheet on my MKI on 26 Oct. Reworking it into an engine on FM Synth's row means redoing the chooser, track_page and render dispatch, plus the setup page and step-lock handling, and then retesting all of it on hardware. That's a lot of rework for something that already works.

You asked whether there's a reason it needs its own row. On the unit, SY DRUM behaves as its own machine: it has its own name in the SRC list and the double-tap list, its own SRC and SETUP pages, and its own step locks and scenes. Users pick it the way they pick FLEX or STATIC. Hiding it behind FM Synth's engine list would make it the one machine you can't choose directly.

Since FRACT and WAVE (#1028) need rows as well, could the base raise ot_machines' room once, to 8 for example? That covers SY DRUM and the machines waiting, without changing how a row works. If that's a problem for the table contract, tell me what limits it and I'll work within that.

On sharing one lock table: I'm open to merging them in a future version. The current build was made and tested before I knew that, with separate tables and their own card files, and card builds have the room, so I'd rather ship it as it is. Since SY DRUM ships in card builds only and FM Synth's USB package stays at 0.1.2, the size doesn't matter for USB yet, so the merge can come in a later update.

@repeat98

Copy link
Copy Markdown
Owner

You're right, Tim. I said option 1 on 26 Oct and you built to it, so let's keep it: SY DRUM stays inside FM Synth with its own chooser row, and I withdraw the engine-on-FM-Synth's-row suggestion. A machine people pick directly, like FLEX or STATIC, belongs in the list as its own row.

That needs two changes in the base, and I'll make both in one base PR and tag you:

Two lock tables are fine for now, since SY DRUM ships in card builds only. Merging them can come in a later update. The SETUP editor question from my last comment still stands: please check whether track_page with SETUP_ALL covers 0x4003a524 before we add a field for it.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks Jannik, that's great, and thanks for the base PR for SY DRUM. Here are the seven quantizer sites, checked against current main (3b79e797): five on MIDI paths and two on audio paths. None of them overlaps a span the base owns, and none is covered by the events you named.

Hook Address Bytes What it does Existing-event coverage
qz_mknob 0x40055382 8 Steps MIDI NOTE and chord-offset knob edits by scale degree before storing the value. Not covered. ev_arp_setup_knob (0x4007a2ec, 8 bytes) handles ARP SETUP, a different editor.
qz_mplock 0x40050b98 10 Steps held-trig MIDI NOTE and chord-offset locks by scale degree. Not covered. ev_plock_store (0x40050e60, 10 bytes) owns the separate audio lock-store site.
qz_mkey 0x400436a8 10 Resolves a MIDI chromatic key press before note-on, arp insertion, held-note storage and recording. Not covered. The sequencer note event does not provide this key-press context.
qz_mnote 0x4009fb80 6 Snaps each sequenced/chord/arp note after transpose and KEY, before note bookkeeping. Not covered. ev_midi_note_key (0x4009fb58, 6 bytes) runs before KEY and can continue at this site; it does not own this later span.
qz_atrig 0x4000c614 6 Snaps resolved STATIC/FLEX audio trig pitch after locks, without rewriting the stored pattern. Not covered. This is an audio-frame path, not a MIDI-note or setup event.
qz_mprev 0x400439d6 8 Snaps the four-note MIDI trig preview on both press and release. Not covered. No existing event owns this preview span.
qz_heldmark 0x40044abc 8 Positions the audio chromatic keyboard's held-key marker at the sounding note. Not covered. ev_arp_setup_text (0x4007a1ec, 6 bytes) draws ARP SETUP values, not this keyboard marker.

I'll keep those seven as guarded patches in Scale Quantizer only. FM Synth on its own will keep SCALE, ROOT and GLIDE in PROJECT > CONTROL > SEQUENCER, with one SCALE for all FM tracks and no per-track rows, and Scale Quantizer keeps its full per-track rows.

One question before I build that: I'd like FM Synth and Scale Quantizer to install side by side, with one set of scale controls on the page. That needs more SEQUENCER rows than the 8 the base allows today, since Scale Quantizer adds 19, and the two per-track scale masks persisted with the other scale settings. Would you accept a base PR from me that raises the added-row limit to at least 19 and persists those masks? If yes, I'll send it together with both module updates. If not, the two stay declared as conflicting, as they are now.

On the SETUP editor at 0x4003a524: I checked, and track_page with SETUP_ALL doesn't cover it. SY DRUM's handler there also turns edits made while a trig or a scene is held into step or scene locks, and releases a playing step's lock before an ordinary edit; SETUP_ALL only lets encoder E edit the machine's own slot. So the field in the machine record you offered would be the simplest route.

@repeat98

Copy link
Copy Markdown
Owner

Thanks Tim, that's exactly what I needed.

Scale rows. Let's keep FM Synth and Scale Quantizer declared as conflicting for now, as they are, so no base PR for the extra SEQUENCER rows. FM Synth with one SCALE for all FM tracks, and Scale Quantizer with its per-track rows and the seven guarded patches, sounds right.

SETUP editor. Thanks for checking. I'll add the field to the machine record in the same base PR that lets one module contribute several ot_machines records, and tag you there. The other half is already done: SYNTIZ (#1028) raised the room to eight contributed rows.

This PR still sits on 13 October's main. When you build the quantizer split, please start from a fresh branch off current main, as in my 25 October note. Push it here or open a new PR, whichever is easier for you.

@repeat98

Copy link
Copy Markdown
Owner

@timhastie, the LFO-names base fix is up as #1080: ot_machines now owns the whole 8-byte lookup at 0x4003bff2 and asks mw_page first, so over USB an FM track's LFO SETUP lists the page your track_page returns. In the emulator an Analog BD track now reads its own names (SYN / TONE / SAT), where it showed FLEX's before. Your card build can keep its LFO-names detour, since card builds have no base: it now starts inside a site the table owns, so list it under the ot_machines contribution's replaces and the USB package drops it.

@timhastie

timhastie commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks Jannik, and thanks for doing the LFO SETUP fix in #1080. I'll list the card build's LFO-names detour under the ot_machines contribution's replaces, as you suggested.

Agreed on keeping FM Synth and Scale Quantizer declared as conflicting for now.

One update on the rows: Scale Quantizer now needs only five SEQUENCER rows instead of 19. They are SCALE, ROOT and GLIDE as before, plus TRACK, which picks a track, and FOLLOW, which sets that track ON or OFF. The per-track settings save through the module's own project lines as before. Five rows fit within the eight contributed rows, so no base change is needed for them.

I'm now working on letting FM Synth and Scale Quantizer install together. They would run one shared quantizer with a single set of those five rows, so there would be no duplicate SCALE, ROOT or GLIDE. I'm building it, and the quantizer split, on fresh branches off current main. Once it passes in the emulator and on my unit, I'll share the results and ask whether the conflict can be lifted for the pair.

Copy link
Copy Markdown
Owner

Hi Tim, thanks for the update. One thing before you build the install-together version, so it doesn't cost you a rebuild: two modules can't share one quantizer. If FM Synth and Scale Quantizer run a single quantizer with one set of rows when both are installed, one hooks into the other's code and rows, and a module may not hook into another module's code, rows, tables or files. Shared code would have to go into the base or the shared SDK first. So let's keep them declared as conflicting, as we agreed on 30 October, and send the FM Synth re-cut and the Scale Quantizer split as their own PRs off current main. I'll close this one when the FM Synth PR is up.


Generated by Claude Code

@timhastie

Copy link
Copy Markdown
Contributor Author

Hi Jannik, understood on 31 October: the shared-quantizer build is withdrawn and the two stay declared as conflicting. I'd still like users to be able to have FM Synth and Scale Quantizer on one unit, so here is the dilemma and the routes I can see. I'd like your view on which to take, and whether there is one I haven't thought of.

Why FM Synth keeps a quantizer: its chords are built on scale degrees, its chromatic keys and legato follow the scale, and GLIDE slews between scale notes. Without the scale in FM Synth, mono tracks would still snap through the PTCH word, but chords, legato and GLIDE would lose the scale, and I don't want to take that from people using it.

So both modules need the same scale state and key sites. Card builds have no base, so I've marked what each route gives.

  1. One module: FM Synth carries the full quantizer for all tracks, as the one-module case your guide allows. Scale Quantizer stays as the standalone, the two stay conflicting, and card users who want both pick FM Synth. No base work. It goes against your 29 October ask to keep MIDI quantizing in Scale Quantizer only, so only with your OK.

  2. A base-owned scale: the base keeps SCALE, ROOT, GLIDE, TRACK and FOLLOW as its rows, the saved bytes and project lines, and exports one accessor, "snap this note for this track", as ev_trig_muted does for muting. FM Synth drops its copy and asks the base; Scale Quantizer keeps the sample and MIDI track snaps. USB only; on a card the pair stays conflicting. I'd write the base PR myself off current main and wait my turn.

  3. Both: route 1 on the card, route 2 over USB.

  4. Leave it: keep the conflict, send the two PRs as agreed, and point people who want both at my own Octatrick build.

Which would you take? Or if you have another idea let me know. Either way the FM Synth re-cut and the Scale Quantizer split come first, off current main.

@repeat98

repeat98 commented Nov 1, 2026

Copy link
Copy Markdown
Owner

Hi Tim, thanks for laying the routes out so clearly. Route 4 for now: keep the two declared as conflicting and send the FM Synth re-cut and the Scale Quantizer split as agreed. Route 1 would put MIDI quantizing back into FM Synth, and I'd rather keep that in Scale Quantizer.

Route 2 is the right shape once more modules want it: a base-owned scale with one "snap this note for this track" accessor could also serve VECTOR and the MIDI generators. Could you open it as a hook proposal issue, with the routes and the modules you see using it? We'll build it when a third module needs it. Card builds keep the conflict either way.

@timhastie

Copy link
Copy Markdown
Contributor Author

Thanks Jannik. Route 4 it is: the FM Synth re-cut and the Scale Quantizer split come as two PRs off current main, declared conflicting, with the Tuner as a third. The hook proposal for the base-owned scale is #1145, with the routes and the modules I see using it.

@timhastie

Copy link
Copy Markdown
Contributor Author

The FM Synth 0.1.4 re-cut is up as #1146, off current main, with Scale Quantizer 0.1.4 as #1147 and Tuner 0.1.0 as #1148. This one can close whenever you like.

@repeat98

repeat98 commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thanks Tim. Closing this one now that the re-cut is up as #1146, with Scale Quantizer 0.1.4 in #1147 and the Tuner in #1148. The review carries on there.


Generated by Claude Code

@repeat98 repeat98 closed this Nov 2, 2026
repeat98 pushed a commit that referenced this pull request Nov 5, 2026
…r: a module may not hook into another module's code, rows, tables or files); FM Synth stays a declared conflict in one sentence; on main 00ecaf5 the image rebuilds byte-identical to TEST2, the author's MKI report of 30 October is its record
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants